using System;
using System.Data;
using System.Diagnostics;
using System.IO;
using System.Text;
using Agile.Common;
using Agile.Common.UI;
using Agile.Genie.Descriptors;
using GeneratorCustomization;

namespace Agile.Genie.Generators
{
    /// <summary>
    /// Generates a NAnt build file for a given project file.
    /// </summary>
    [GUIDetails("Build File")]
    public class BuildFileGenerator : Generator
    {
        #region Constructors and Factories

        /// <summary>
        /// Constructor
        /// </summary>
        private BuildFileGenerator(FileInfo projectFile)
        {
            _projectFile = AgileFileInfo.Build(projectFile);
        }

        /// <summary>
        /// Instantiate a new Build file generator for the given solution file.
        /// </summary>
        /// <remarks>File type must be a project file, if it is not,
        /// null will be returned.
        /// <p>Will also return null if the project file is in the 'excluded' list.</p></remarks>
        /// <returns>Returns null if the file is not a project file, otherwise returns the instantiated generator.</returns>
        public static BuildFileGenerator Build(FileInfo projectFile)
        {
            if (!Files.IsTheRightFileType(projectFile, "csproj", "vbproj"))
                return null;
            if (!projectFile.Exists)
                return null;

            // If an exception occurs during instantiation, just return null
            try
            {
                var generator = new BuildFileGenerator(projectFile);
                if (generator.IsExcludedProject)
                    return null;
                return generator;
            }
            catch (Exception ex)
            {
                Debug.WriteLine(ex.Message);
                return null;
            }
        }

        #endregion

        private readonly AgileFileInfo _projectFile;
        private VisualStudioProjectFile _visualStudioProjectFile;
        private DataTable serverTestingProjects;

        /// <summary>
        /// Checks if the project file is in the excluded projects list in the Generator Details Dataset.
        /// </summary>
        /// <remarks>Excluded projects are ones that we do not want to generate a build file for
        /// (and also do not want included in the master.build).
        /// </remarks>
        /// <returns>True if the project has been listed explicitly to be excluded.</returns>
        private bool IsExcludedProject
        {
            get
            {
                // Ignore all .Tests projects
                if (ProjectFile.Name.Contains(".Test"))
                    return true;

                foreach (DataRow row in GeneratorsData.All.Tables["ProjectsExcludedFromBuild"].Rows)
                {
                    if (ProjectFile.NameExcludingExtension == row["ProjectName"].ToString())
                        return true;
                }
                return false;
            }
        }

        /// <summary>
        /// Gets the project system file that we are generating the .build file for.
        /// </summary>
        public FileInfo ProjectFileInfo
        {
            get { return ProjectFile.FileInfo; }
        }

        /// <summary>
        /// Gets the AgileFileInfo object that encapsulates the project file.
        /// </summary>
        public AgileFileInfo ProjectFile
        {
            get { return _projectFile; }
        }

        /// <summary>
        /// Gets the visual studio project files.
        /// </summary>
        private VisualStudioProjectFile VisualStudioProjectFile
        {
            get
            {
                if (_visualStudioProjectFile == null)
                {
                    _visualStudioProjectFile = VisualStudioProjectFileFactory.Build(ProjectFileInfo);
                }
                return _visualStudioProjectFile;
            }
        }

        /// <summary>
        /// Returns the name of the output file that will be generated by the msbuild process
        /// </summary>
        private static string OutputFileName
        {
            get
            {
                // Don't append anything for Compact framework...doesn't matter if it overwrites (if the CF results overwrites the normal build results it means the normal build was successful)
                // Cruise Control can only grab one results file.
                return "buildResults.xml";
            }
        }


        /// <summary>
        /// Generates the build file core code
        /// </summary>
        /// <returns></returns>
        public string GenerateBuildFileCode()
        {
            string code =string.Format(@"
<project name=""{0}"" default=""go"" basedir=""."">
		<property name=""TestsBaseDirectory"" value=""{7}"" />
		<property name=""TestsAssemblyDirectory"" value=""${{TestsBaseDirectory}}\bin\Release"" />
		<property name=""RunDirectory"" value=""${{project::get-base-directory()}}\${{project::get-name()}}"" />
        <property name=""BuildResultsDirectory"" value=""${{directory::get-current-directory()}}\Build"" />
        <property name=""nant.onfailure"" value=""OnFailure"" />

	<!-- Go
		Runs both the build and test targets.
	  --> 
	<target name=""go"" depends=""build{6}, test, UpdateResults""/>
	
	<!-- Build
		Builds the project and test project (if there is one).
	 -->
{4}
{5}

	<!-- test
		Tests are compulsory - every project must have them. All test results are copied into
		C:\build\testresults (or whatever your RootDirectory is)
	  -->
	  
{1}
{2}
{3}
</project>
"
                    , FileName
                    , GenerateTestSection()
                    , GenerateCopyResults()
                    , GenerateOnFailure()
                    , GenerateBuildTarget()
                    , GenerateBuildTestTarget()
                    , (!VisualStudioProjectFile.IsUnitTested || !VisualStudioProjectFile.HasTestProject) ? string.Empty : ", buildTest"
                    , GetTestProjectFileDirectoryOnly()
                    );
            return code;
        }

        /// <summary>
        /// Gets the command line args for the msbuild execution.
        /// </summary>
        /// <returns></returns>
        private string GenerateBuildTarget()
        {
            // Set the command line details (for msbuild) differently for CF and normal projects. 
            string commandLine = VisualStudioProjectFile.IsCompactFramework 
                                     ? string.Format(@"{0}", FileName.Replace("Compact", "Compact") + ProjectFile.FileInfo.Extension) 
                                     : string.Format(@"{0}", string.Format("{0}{1}", FileName, ProjectFile.FileInfo.Extension));

            return GetBuildTarget("build", commandLine);
        }

        /// <summary>
        /// Gets the command line args for the msbuild execution.
        /// </summary>
        /// <returns></returns>
        private string GenerateBuildTestTarget()
        {
                // If it does not have a unit test project, dont create the 'buildTest' target
            if (!VisualStudioProjectFile.IsUnitTested || !VisualStudioProjectFile.HasTestProject)
                return string.Empty;

            // Default to building the test project (doing this ensures the test project gets built which triggers a build of the project itself if required)
            return GetBuildTarget("buildTest",  GetTestProjectFileLocation());

//            return GetBuildTarget("buildTest", string.Format(@"""..\${{project::get-name()}}.Test\{0}"
//                , string.Format("{0}.Test{1}", FileName, ProjectFile.FileInfo.Extension)));
        }

        /// <summary>
        /// Returns the directory that the test project file is in 
        /// </summary>
        private string GetTestProjectFileLocation()
        {
            FileInfo testProject = VisualStudioProjectFile.GetTestProjectFile();
            if (testProject == null || testProject.Directory == null)
                return string.Empty;

            // don't get the full path as build files get checked in and will likely be in a different dir on the build box and other developers machines.
            string testProjectName = testProject.Name;
            return string.Format(@"{0}\{1}", GetTestProjectFileDirectoryOnly(), testProjectName);
        }

        /// <summary>
        /// Returns the directory that the test project file is in 
        /// </summary>
        private string GetTestProjectFileDirectoryOnly()
        {
            FileInfo testProject = VisualStudioProjectFile.GetTestProjectFile();
            if (testProject == null || testProject.Directory == null)
                return string.Empty;

            // don't get the full path as build files get checked in and will likely be in a different dir on the build box and other developers machines.
            string testProjectDirectory = testProject.Directory.Name;
            return string.Format(@"..\{0}", testProjectDirectory);
        }

        private string GetBuildTarget(string targetName, string commandLineCode)
        {
            const string rootDriveEnvironmentalVariableName = "RootDrive";
            string platform = VisualStudioProjectFile.IsCompactFramework ? "AnyCPU" : "AnyCPU"; // need tobe different for CF?
            string stuffAtTheEnd =
                string.Format(
                    @" /verbosity:quiet /nologo /p:Configuration=Release;Platform={2} /logger:${{environment::get-variable('{1}')}}:\Build\CCLogger\ThoughtWorks.CruiseControl.MSBuild.dll;{0}""/>""/>"
                    , OutputFileName
                    , rootDriveEnvironmentalVariableName
                    , platform);

            return string.Format(@"
	<target name=""{0}"">
		<delete file=""{2}"" failonerror=""false""/>
		<echo message=""***  Building {1}...""/>
    	<exec 
			program=""msbuild"" 
			commandline=""{1}{3}
		<echo message=""***  Build of {1} Completed Successfully!""/>
	</target>
"
                , targetName
                , commandLineCode
                , OutputFileName
                , stuffAtTheEnd
                );
        }


        /// <summary>
        /// Generate the CopyResults target
        /// </summary>
        /// <returns></returns>
        private static string GenerateCopyResults()
        {
            return string.Format(@"
	<target name=""UpdateResults"">
		<mkdir dir=""${{BuildResultsDirectory}}"" failonerror=""false""/>
        <copy verbose=""true"" todir=""${{BuildResultsDirectory}}"" overwrite=""true"">
			<fileset basedir=""${{project::get-base-directory()}}"">
				<include name=""buildResults.xml""/>
			</fileset>
		</copy>
	</target>

	<target name=""CopyResults"">
		<mkdir dir=""${{BuildResultsDirectory}}\TestResults"" failonerror=""true""/>
		<copy verbose=""true"" todir=""${{BuildResultsDirectory}}\TestResults"">
			<fileset basedir=""${{TestsAssemblyDirectory}}"">
				<include name=""*results.xml""/>
			</fileset>
		</copy>
		<echo message=""copied test results from: ${{TestsAssemblyDirectory}}...TO --> ${{BuildResultsDirectory}}""></echo>
	</target>
");
        }

        /// <summary>
        /// Generate the OnFailure target
        /// </summary>
        /// <returns></returns>
        private string GenerateOnFailure()
        {
            var onFailure =
                new StringBuilder(
                    string.Format(
                        @"
    <target name=""OnFailure"" description=""Called when build fails"">
        <property name=""message"" value=""OnFailure""/>
        <call target=""CopyResults"" />
        
		<call target=""UpdateResults"" />
"
                        , OutputFileName));

            if (ProjectIsServerTesting(VisualStudioProjectFile.ProjectName))
            {
                onFailure.Append(
                    string.Format(
                        @"
		<mail 
          from=""isisclient@aus.fujixerox.com"" 
          tolist=""mark.wallis@aus.fujixerox.com; ali.shafai@aus.fujixerox.com; dave.wheeler@aus.fujixerox.com; craig.hunter@aus.fujixerox.com"" 
          subject=""Tests failed in ${{project::get-name()}}"" 
          mailhost=""ausnry-ex01.pilot.local""
          message=""Note: this mail is also generated when a compile error occurs."">
          <attachments>
              <include name=""${{TestsAssemblyDirectory}}\*Results.xml"" />
          </attachments>
       </mail>
"));
            }

            onFailure.Append(@"
    </target>
");
            return onFailure.ToString();
        }


        /// <summary>
        /// Returns true if the tests for the project test the server.
        /// </summary>
        /// <remarks>COPIED IN MasterBuildFileGenerator</remarks>
        /// <returns></returns>
        private bool ProjectIsServerTesting(string projectName)
        {
            if (serverTestingProjects == null)
                serverTestingProjects = GeneratorsData.GetDataTable("ServerTestingProjects");

            foreach (DataRow row in serverTestingProjects.Rows)
            {
                if (row["ProjectName"].ToString() == projectName)
                    return true;
            }

            return false;
        }


        /// <summary>
        /// Generates the test section of the build file
        /// </summary>
        /// <returns></returns>
        public string GenerateTestSection()
        {
            if (VisualStudioProjectFile.OutputType.EndsWith("Exe"))
                return GenerateTestsNotRun("TESTS NOT CURRENTLY RUN FOR PROJECTS WITH AN Exe Output");

            if (!VisualStudioProjectFile.IsUnitTested)
                return GenerateTestsNotRun("PROJECTS OF THIS TYPE ARE NOT UNIT TESTED!");

            if (!VisualStudioProjectFile.HasTestProject)
                return GenerateTestsNotRun("THIS PROJECT DOES NOT HAVE A .TEST PROJECT!");

            return GenerateTestSectionDetails();
        }

        /// <summary>
        /// Generates the test section of the build file for VS2003 projects
        /// </summary>
        /// <returns></returns>
        public string GenerateTestSectionDetails()
        {
            FileInfo testProjectFile = VisualStudioProjectFile.GetTestProjectFile();
            string code = string.Format(
                    @"    <target name=""test"">
    <echo message=""*******************************************************""/>
    <echo message="" RUNNING TEST SUITES                                  *""/>
    <echo message=""     - {0}\{1}.dll""/>
    <echo message=""                                                      *""/>
    <echo message=""*******************************************************""/>

        <nunit2 verbose=""true"" failonerror=""true"">
        <formatter type=""Plain"" />
        <formatter type=""Xml"" usefile=""true"" extension="".xml"" />
        <test assemblyname=""${{TestsAssemblyDirectory}}\{1}.dll""/>
    </nunit2>

	<call target=""CopyResults"" />
    <echo message=""""/>
    <echo message=""***  ${{project::get-name()}} SUCCESSFULLY BUILT AND TESTED!  ***""/>
    <echo message=""""/>
        
	</target>"
                    , GetTestProjectFileDirectoryOnly()
                    , testProjectFile.Name.Replace(".csproj", string.Empty).Replace(".vbproj", string.Empty)
                    );
            return code;
        }

        /// <summary>
        /// Generates the test section but just reports in NAnt that tests are not run.
        /// </summary>
        /// <returns></returns>
        public string GenerateTestsNotRun(string reason)
        {
            string code =
                string.Format(
                    @"
    <target name=""test"">
        <echo message=""*******************************************************""/>
        <echo message="" {0}      *""/>
        <echo message=""                                                      *""/>
        <echo message=""                   !!TESTS NOT RUN!!                  *""/>
        <echo message=""*******************************************************""/>        
    </target>",
                    reason);
            return code;
        }


        /// <summary>
        /// Gets the list of comma separated dependencies.
        /// </summary>
        /// <example>For Generators:
        /// <p>'Common, Descriptors'</p>
        /// </example>
        /// <returns>string containing comma separated list of the projects dependencies.</returns>
        public string GetCommaSeparatedDependencies()
        {
            var dependencies = new StringBuilder();
            try
            {
                foreach (VisualStudioProjectFile referencedAssembly in VisualStudioProjectFile.ReferencedProjects)
                {
                    if (!IsAnExcludedAssembly(referencedAssembly.ProjectAssemblyName))
                        dependencies.Append(string.Format(@", {0}", referencedAssembly.ProjectName));
                }
            }
            catch (Exception ex)
            {
                dependencies.Append(string.Format("    An Exception occured trying to get dependencies [{0}]",
                                                  ex.Message));
            }

            return Strings.RemoveFirstInstanceOf(",", dependencies.ToString()).Trim();
        }

        /// <summary>
        /// Determines if the assembly is in the list of assemblies that are not to be
        /// included in the build file dependency list.
        /// </summary>
        /// <remarks>This is important because we don't want to include system and third 
        /// party assemblies in our dependency list for build files (we only want to know
        /// which of OUR assemblies must be built first!)</remarks>
        /// <param name="assemblyName">Check if this assembly is to be excluded.</param>
        /// <returns>Returns true if it is to be excluded.</returns>
        public bool IsAnExcludedAssembly(string assemblyName)
        {
            foreach (DataRow excluded in GeneratorsData.GetDataRowsFor("BuildFileGeneratorExcludedDependencies"))
            {
                string excludeAssembliesStartingWith = excluded["StartingWith"].ToString();
                if (assemblyName.StartsWith(excludeAssembliesStartingWith))
                    return true;
            }
            return false;
        }

        #region Overrides

        /// <summary>
        /// Gets the string that is used as the project dependency 'heading'.
        /// </summary>
        /// <remarks>Declared as public static because the MasterBuildFileGenerator accesses the value.
        /// Don't normally like to do this but that generator uses BuildFile generated by this
        /// class to do it's generation work. However if it needs to get any other info, better
        /// to make a Base BuildFileGen class...</remarks>
        public static string ProjectDependencyString = "This Project is dependent on:";

        /// <summary>
        /// Gets the base directory for file generation.
        /// </summary>
        /// <remarks>i.e. The base directory that the file will be generated in.</remarks>
        public override DirectoryInfo FileGenerationDirectory
        {
            get { return ProjectFile.FileInfo.Directory; }
        }

        /// <summary>
        /// Gets the additional information that is required to save the generated file
        /// in the correct directory.
        /// e.g. "Northwind\SQL\StoredProcs"
        /// </summary>
        /// <remarks>i.e. The base directory that the file will be generated in.</remarks>
        protected override string FileGenerationDirectoryDetails
        {
            get { return "NOT USED FOR BUILD FILE GENERATOR, FOR THIS WE JUST USE THE DIR OF THE SLN FILE."; }
        }

        /// <summary>
        /// Gets the description of the purpose of the file.
        /// </summary>
        protected override string FilePurpose
        {
            get
            {
                return string.Format(@"{0} Build File
Compiles and tests the project in Release mode.
{1}
{2}"
                                     , FileName
                                     , ProjectDependencyString
                                     , GetCommaSeparatedDependencies());
            }
        }

        /// <summary>
        /// Gets a string containing additional notes about the files contents.
        /// </summary>
        protected override string FileNotes
        {
            get { return string.Empty; }
        }

        /// <summary>
        /// Gets the file extension of the file that is to be created.
        /// </summary>
        /// <example>.sql</example>
        public override string FileExtension
        {
            get { return ".build"; }
        }

        /// <summary>
        /// Gets the file name of the file that is to be created, not including the file extension.
        /// </summary>
        /// <example>fileThatIsBeingGenerated</example>
        public override string FileName
        {
            get { return ProjectFileInfo.Name.Replace(ProjectFileInfo.Extension, ""); }
        }

        /// <summary>
        /// Overrides the GeneratedStamp to put the comment tags in front.
        /// </summary>
        public override string GeneratedStamp
        {
            get { return "<!-- " + base.GeneratedStamp + " -->"; }
        }

        /// <summary>
        /// Generate all of the code.
        /// </summary>
        /// <returns></returns>
        public override string Generate()
        {
            try
            {
                string baseCode = base.Generate();
                var generatedCode = new StringBuilder(baseCode);
                generatedCode.Append(GenerateBuildFileCode());

                Debug.WriteLine(string.Format(@"Generated build file for {0}", VisualStudioProjectFile.ProjectName));
                return generatedCode.ToString();
            }
            catch (Exception ex)
            {
                Debug.WriteLine(ex.Message);
                return new StringBuilder(
                    string.Format("<!-- An exception occurred whilst generating:- \r\n \r\n{0} -->", ex.Message)).
                    ToString();
            }
        }

        /// <summary>
        /// Generates the comments section for the file
        /// </summary>
        /// <returns></returns>
        public override string GenerateFileHeaderComments()
        {
            return string.Format(@"
<!-- {0}
-->
"
                                 , Strings.SplitMultiLineString(FilePurpose, " "));
        }

        /// <summary>
        /// Initialize all delegates for code generation. 
        /// i.e. set the strategy pattern handlers.
        /// </summary>
        protected override void InitializeDelegates()
        {
            // nothing to initialize for the build file generator.
        }

        #endregion

        #region Old code that was used to get the project file from sln file details. Now just using proj file directly 

        //        /// <summary>
        //        /// Gets the path of the project file from the solution file.
        //        /// NOTE: Assumes that the project and solution files have the same name
        //        /// (not including the extension) and the project is in the solution.
        //        /// </summary>
        //        /// <returns></returns>
        //        private string GetProjectFileFullPath()
        //        {
        //            string solutionText = Files.GetFileContents(ProjectFileInfo);
        //            string initialReference = string.Format(@"""{0}"","
        //                                                    , ProjectFile.NameExcludingExtension);
        //            int initialReferenceIndex = solutionText.ToLower().IndexOf(initialReference.ToLower());
        //            int pathStartIndex = solutionText.ToLower().IndexOf(ProjectFile.NameExcludingExtension.ToLower()
        //                                                                , initialReferenceIndex + initialReference.Length);
        //            int pathEndIndex = solutionText.IndexOf(@"""", pathStartIndex);
        //            string projectFileSubPath = solutionText.Substring(pathStartIndex, pathEndIndex - pathStartIndex);
        //            return Path.Combine(ProjectFileInfo.DirectoryName, projectFileSubPath);
        ////            FileInfo file = new FileInfo(Path.Combine(ProjectFileInfo.DirectoryName, projectFileSubPath));
        ////            return AgileFileInfo.Build(file);
        //        }
        //        /// <summary>
        //        /// Gets the path of the project file from the solution file.
        //        /// NOTE: Assumes that the project and solution files have the same name
        //        /// (not including the extension) and the project is in the solution.
        //        /// </summary>
        //        /// <returns></returns>
        //        private string GetProjectFileDirectory()
        //        {
        //            return
        //                Strings.RemoveFirstInstanceOf(ProjectFile.NameExcludingExtension + ".csproj", GetProjectFileFullPath());
        //        }

        #endregion

        /// <summary>
        /// Generate the copy configs section for projects that have config files.
        /// </summary>
        /// <returns></returns>
        [Obsolete]
        private string GenerateCopyConfigs()
        {
            //            if (!VisualStudioProjectFile.NeedsConfigFilesCopied)
            return string.Empty;
            //
            //            return
            //                @"
            //				<echo message=""
            //***  Copying Config files...from: ${directory::get-parent-directory(GrandParent)}\Build\Configs"" />
            //		<copy verbose=""true"" todir=""${RunDirectory}"" overwrite=""true"" failonerror=""true"">
            //			<fileset basedir=""${directory::get-parent-directory(GrandParent)}\Build\Configs"">
            //				<include name=""*configuration.config""/>
            //			</fileset>		
            //		</copy>
            //";
        }

    }
}